<pre class='metadata'>
Title: Web Background Synchronization
Status: CG-DRAFT
ED: https://wicg.github.io/BackgroundSync/spec/
Shortname: background-sync
Level: 1
Editor: Josh Karlin, Google, jkarlin@chromium.org
Editor: Marijn Kruisselbrink, Google, mek@chromium.org
Abstract: This specification describes a method that enables web applications to synchronize data in the background.
Group: wicg
Repository: WICG/BackgroundSync
Link Defaults: html (dfn) allowed to show a popup/event handler idl attribute/global object/in parallel/incumbent settings object/perform a microtask checkpoint/queue a task/script execution environment
</pre>

<pre class=biblio>
{
  "promises-guide": {
    "href": "https://www.w3.org/2001/tag/doc/promises-guide",
    "title": "Writing Promise-Using Specifications",
    "date": "24 July 2015",
    "status": "Finding of the W3C TAG",
    "publisher": "W3C TAG"
  }
}
</pre>

<pre class="anchors">
spec: ecma-262; urlPrefix: http://www.ecma-international.org/ecma-262/6.0/
    type: dfn
        text: Assert; url: sec-algorithm-conventions

spec: html; urlPrefix: https://html.spec.whatwg.org/
    type: dfn
        text: trusted; url: concept-events-trusted
        text: browsing context; url: browsing-context

spec: powerful-features; urlPrefix: https://w3c.github.io/webappsec/specs/powerfulfeatures/#
    type: dfn
        text: secure context; url: secure-context

spec: promises-guide; urlPrefix: https://www.w3.org/2001/tag/doc/promises-guide#
    type: dfn
        text: A new promise; url: a-new-promise
        text: A promise rejected with; url: a-promise-rejected-with
        text: Reject; url: reject-promise
        text: Resolve; url: resolve-promise
        text: Transforming; url: transforming-by
        text: Upon fulfillment; url: upon-fulfillment
        text: Upon rejection; url: upon-rejection
        text: Waiting for all; url: waiting-for-all

spec: service-workers; urlPrefix: https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html
    type: dfn
        text: active worker; url: dfn-active-worker
        text: client; url: dfn-service-worker-client
        text: control; url: dfn-control
        text: extended lifetime promises; url: dfn-extend-lifetime-promises
        text: frame type; url: dfn-service-worker-client-frame-type
        text: handle functional event; url: handle-functional-event-algorithm
        text: installing worker; url: dfn-installing-worker
        text: service worker; url: service-worker-concept
        text: service worker client; url: dfn-service-worker-client
        text: service worker registration; url: service-worker-registration-concept
        text: termination; url: terminate-service-worker-algorithm
        text: waiting worker; url: dfn-waiting-worker
    type: interface
        text: ExtendableEvent; url: extendable-event-interface
        text: ExtendableEventInit; url: extendable-event-init-dictionary
        text: ServiceWorkerGlobalScope; url: service-worker-global-scope-interface
        text: ServiceWorkerRegistration; url: service-worker-registration-interface

spec: WebIDL; urlPrefix: https://heycam.github.io/webidl/#
    type: exception
        text: AbortError; url: aborterror
        text: DOMException; url: idl-DOMException
        text: InvalidAccessError; url: invalidaccesserror
        text: InvalidStateError; url: invalidstateerror
        text: NotAllowedError; url: notallowederror
    type: interface
        text: DOMString; url: idl-DOMString
        text: sequence; url: idl-sequence
</pre>

<section>
  <h2 id='introduction'>Introduction</h2>

  <em>This section is non-normative.</em>

  Web Applications often run in environments with unreliable networks (e.g., mobile phones) and unknown lifetimes (the browser might be killed or the user might navigate away). This makes it difficult to synchronize client data from web apps (such as photo uploads, document changes, or composed emails) with servers. If the browser closes or the user navigates away before synchronization can complete, the app must wait until the user revisits the page to try again. This specification provides a new onsync <a>service worker</a> event which can fire <a>in the background</a> so that synchronization attempts can continue despite adverse conditions when initially requested. This API is intended to reduce the time between content creation and content synchronization with the server.

  As this API relies on service workers, functionality provided by this API is only available in a <a>secure context</a>.

  <div class="example">
    Requesting a background sync opportunity from a <a>browsing context</a>:

    <pre class="lang-js">
      function sendChatMessage(message) {
        return addChatMessageToOutbox(message).then(() => {
          // Wait for the scoped service worker registration to get a
          // service worker with an active state
          return navigator.serviceWorker.ready;
        }).then(reg => {
          return reg.sync.register('send-chats');
        }).then(() => {
          console.log('Sync registered!');
        }).catch(() => {
          console.log('Sync registration failed :(');
        });
      }
    </pre>

    In the above example <code>addChatMessageToOutbox</code> is a developer-defined function.

    Reacting to a sync event within a <a>service worker</a>:

    <pre class="lang-js">
      self.addEventListener('sync', event => {
        if (event.tag == 'send-chats') {
          event.waitUntil(
            getMessagesFromOutbox().then(messages => {
              // Post the messages to the server
              return fetch('/send', {
                method: 'POST',
                body: JSON.stringify(messages),
                headers: { 'Content-Type': 'application/json' }
              }).then(() => {
                // Success! Remove them from the outbox
                return removeMessagesFromOutbox(messages);
              });
            }).then(() => {
              // Tell pages of your success so they can update UI
              return clients.matchAll({ includeUncontrolled: true });
            }).then(clients => {
              clients.forEach(client => client.postMessage('outbox-processed'))
            })
          );
        }
      });
    </pre>

    In the above example <code>getMessagesFromOutbox</code> and <code>removeMessagesFromOutbox</code> are developer-defined functions.
  </div>
</section>

<section>
  <h2 id="concepts">Concepts</h2>

  The sync event is considered to run <dfn>in the background</dfn> if no <a>service worker clients</a> whose <a>frame type</a> is top-level or auxiliary exist for the origin of the corresponding service worker registration.

  The user agent is considered to be <dfn>online</dfn> if the user agent has established a network connection. A user agent MAY use a stricter definition of being <a>online</a>. Such a stricter definition MAY take into account the particular <a>service worker</a> or origin a <a>sync registration</a> is associated with.
</section>

<section>
  <h2 id="constructs">Constructs</h2>
  A <a>service worker registration</a> has an associated <dfn>list of sync registrations</dfn> whose element type is a <a>sync registration</a>.

  A <dfn>sync registration</dfn> is a tuple consisting of a <a>tag</a> and a <a lt="registration state">state</a>.

  A <a>sync registration</a> has an associated <dfn>tag</dfn>, a DOMString.

  A <a>sync registration</a> has an associated <dfn>registration state</dfn>, which is one of <dfn>pending</dfn>, <dfn>waiting</dfn>, <dfn>firing</dfn>, or <dfn>reregisteredWhileFiring</dfn>. It is initially set to <a>pending</a>.

  A <a>sync registration</a> has an associated <a>service worker registration</a>. It is initially set to null.

  Within one <a>list of sync registrations</a> each <a>sync registration</a> MUST have a unique <a>tag</a>.
</section>

<section>
  <h2 id="privacy-considerations">Privacy Considerations</h2>

  <section>
    <h3 id="permission">Permission</h3>
    User agents MAY offer a way for the user to disable background sync.

    Note: Background sync SHOULD be enabled by default. Having the permission denied is considered an exceptional case.
  </section>

  <section>
    <h3 id="location-tracking">Location Tracking</h3>
    Fetch requests within the onsync event while <a>in the background</a> may reveal the client's IP address to the server after the user left the page. The user agent SHOULD limit tracking by capping the number of retries and duration of sync events.
  </section>

  <section>
    <h3 id="history-leaking">History Leaking</h3>
    Fetch requests within the onsync event while <a>in the background</a> may reveal something about the client's navigation history to passive eavesdroppers. For instance, the client might visit site https://example.com, which registers a sync event, but doesn't fire until after the user has navigated away from the page and changed networks. Passive eavesdroppers on the new network may see the fetch requests that the onsync event makes. The fetch requests are HTTPS so the request contents will not be leaked but the domain may be (via DNS lookups and IP address of the request).
  </section>
</section>

<section>
  <h2 id="api-description">API Description</h2>

  <section>
    <h3 id="service-worker-registration-extensions">Extensions to the {{ServiceWorkerRegistration}} interface</h3>

    <pre class="idl">
      partial interface ServiceWorkerRegistration {
        readonly attribute SyncManager sync;
      };
    </pre>

    The <code><dfn attribute for=SyncManager title=sync>sync</dfn></code> attribute exposes a {{SyncManager}}, which has an associated <a>service worker registration</a> represented by the {{ServiceWorkerRegistration}} on which the attribute is exposed.
  </section>

  <section>
    <h3 id="sync-manager-interface">{{SyncManager}} interface</h3>

    <pre class="idl">
      [Exposed=(Window,Worker)]
      interface SyncManager {
        Promise&lt;void&gt; register(DOMString tag);
        Promise&lt;sequence&lt;DOMString&gt;&gt; getTags();
      };
    </pre>

    The <code><dfn method for=SyncManager title="register(tag)">register(<var>tag</var>)</dfn></code> method, when invoked, MUST return <a>a new promise</a> <var>promise</var> and run the following steps <a>in parallel</a>:
    <ol>
      <li>
        Let <var>serviceWorkerRegistration</var> be the {{SyncManager}}'s associated <a>service worker registration</a>.
      </li>
      <li>
        If |serviceWorkerRegistration|'s [=active worker=] is null, [=reject=] |promise| with an {{InvalidStateError}} and abort these steps.
      </li>
      <li>
        If the user has disabled background sync, <a>reject</a> <var>promise</var> with an {{NotAllowedError}} and abort these steps.
      </li>
      <li>
        Let <var>isBackground</var> be true.
      </li>
      <li>
        For each <var>client</var> in the <a>service worker clients</a> for the <var>serviceWorkerRegistration</var>'s origin:
        <ol>
          <li>
            If <var>client</var>'s <a>frame type</a> is top-level or auxiliary, set <var>isBackground</var> to false.
          </li>
        </ol>
      </li>
      <li>
        If <var>isBackground</var> is true, <a>reject</a> <var>promise</var> with an {{InvalidAccessError}} and abort these steps.
      </li>
      <li>
        Let <var>currentRegistration</var> be the <a lt="sync registration">registration</a> in <var>serviceWorkerRegistration</var>'s <a>list of sync registrations</a> whose <a>tag</a> equals <var>tag</var> if it exists, else null.
      </li>
      <li>
        If <var>currentRegistration</var> is not null:
        <ol>
          <li>
            If <var>currentRegistration</var>'s <a>registration state</a> is <a>waiting</a>, set <var>currentRegistration</var>'s <a>registration state</a> to <a>pending</a>.
          </li>
          <li>
            If <var>currentRegistration</var>'s <a>registration state</a> is <a>firing</a>, set <var>currentRegistration</var>'s <a>registration state</a> to <a>reregisteredWhileFiring</a>.
          </li>
          <li>
            <a>Resolve</a> <var>promise</var>.
          </li>
          <li>
            If the user agent is currently <a>online</a> and <var>currentRegistration</var>'s <a>registration state</a> is <a>pending</a>, <a>fire a sync event</a> for <var>currentRegistration</var>.
          </li>
        </ol>
      </li>
      <li>
        Else:
        <ol>
          <li>
            Let <var>newRegistration</var> be a new <a>sync registration</a>.
          </li>
          <li>
            Set <var>newRegistration</var>'s associated <a>tag</a> to <var>tag</var>.
          </li>
          <li>
            Set <var>newRegistration</var>'s associated <a>service worker registration</a> to <var>serviceWorkerRegistration</var>.
          </li>
          <li>
            Add <var>newRegistration</var> to <var>serviceWorkerRegistration</var>'s <a>list of sync registrations</a>.
          </li>
          <li>
            <a>Resolve</a> <var>promise</var>.
          </li>
          <li>
            If the user agent is currently <a>online</a>, <a>fire a sync event</a> for <var>newRegistration</var>.
          </li>
        </ol>
      </li>
    </ol>

    The <code><dfn method for=SyncManager title="getTags()">getTags()</dfn></code> method when invoked, MUST return <a>a new promise</a> <var>promise</var> and run the following steps <a>in parallel</a>:
    <ol>
      <li>
        Let <var>serviceWorkerRegistration</var> be the {{SyncManager}}'s associated <a>service worker registration</a>.
      </li>
      <li>Let <var>currentTags</var> be a new {{sequence}}.</li>
      <li>
        For each <var>registration</var> in <var>serviceWorkerRegistration</var>'s <a>list of sync registrations</a>, add <var>registration</var>'s associated <a>tag</a> to <var>currentTags</var>.
      </li>
      <li>
        <a>Resolve</a> <var>promise</var> with <var>currentTags</var>.
      </li>
    </ol>
  </section>

  <section>
    <h3 id="sync-event">The sync event</h3>

    <pre class="idl">
      partial interface ServiceWorkerGlobalScope {
        attribute EventHandler onsync;
      };

      [Exposed=ServiceWorker]
      interface SyncEvent : ExtendableEvent {
        constructor(DOMString type, SyncEventInit init);
        readonly attribute DOMString tag;
        readonly attribute boolean lastChance;
      };

      dictionary SyncEventInit : ExtendableEventInit {
        required DOMString tag;
        boolean lastChance = false;
      };
    </pre>

    Note: The {{SyncEvent}} interface represents a firing sync registration. If the page (or worker) that registered the event is running, the user agent will fire the sync event as soon as network connectivity is available. Otherwise, the user agent should run the event at the soonest convenience. If a sync event fails, the user agent may decide to retry it at a time of its choosing. The {{SyncEvent/lastChance}} attribute is true if the user agent will not make further attempts to try this sync after the current attempt.

    <div class="example">
      Reacting to {{SyncEvent/lastChance}}:

      <pre class="lang-js">
        self.addEventListener('sync', event => {
          if (event.tag == 'important-thing') {
            event.waitUntil(
              doImportantThing().catch(err => {
                if (event.lastChance) {
                  self.registration.showNotification("Important thing failed");
                }
                throw err;
              })
            );
          }
        });
      </pre>

      The above example reacts to {{SyncEvent/lastChance}} by showing a <a href="https://notifications.spec.whatwg.org/#concept-notification">notification</a> to the user. This requires the origin to have <a href="https://notifications.spec.whatwg.org/#permission-model">permission to show notifications</a>.

      In the above example <code>doImportantThing</code> is a developer-defined function.
    </div>

    Whenever the user agent changes to <a>online</a>, the user agent SHOULD <a>fire a sync event</a> for each <a>sync registration</a> whose <a>registration state</a> is <a>pending</a>. The events may be fired in any order.

    To <dfn>fire a sync event</dfn> for a <a>sync registration</a> <var>registration</var>, the user agent MUST run the following steps:
    <ol>
      <li>
        <a>Assert</a>: <var>registration</var>'s <a>registration state</a> is <a>pending</a>.
      </li>
      <li>
        Let <var>serviceWorkerRegistration</var> be the <a>service worker registration</a> associated with <var>registration</var>.
      </li>
      <li>
        <a>Assert</a>: <var>registration</var> exists in the <a>list of sync registrations</a> associated with <var>serviceWorkerRegistration</var>.
      </li>
      <li>
        Set <var>registration</var>'s <a>registration state</a> to <a>firing</a>.
      </li>
      <li>
        <p><a>Fire Functional Event</a> "<code>sync</code>" using {{SyncEvent}} on <var>serviceWorkerRegistration</var> with the following properties:</p>

        <dl>
          <dt>{{SyncEvent/tag}}</dt>
          <dd>The <a>tag</a> associated with <var>registration</var></dd>
          <dt>{{SyncEvent/lastChance}}</dt>
          <dd>False if the user agent <a>will retry</a> this sync event if it fails, or true if no further attempts will be made after the current attempt.</dd>
        </dl>

        <p>Then run the following steps with <var>dispatchedEvent</var>:</p>

        <ol>
          <li>
            Let <var>waitUntilPromise</var> be the result of <a>waiting for all</a> of <var>dispatchedEvent</var>'s <a>extended lifetime promises</a>.
          </li>
          <li>
            <a>Upon fulfillment</a> of <var>waitUntilPromise</var>, perform the following steps atomically:
            <ol>
              <li>
                If <var>registration</var>'s state is <a>reregisteredWhileFiring</a>:
                <ol>
                  <li>
                    Set <var>registration</var>'s state to <a>pending</a>.
                  </li>
                  <li>
                    If the user agent is currently <a>online</a>, <a>fire a sync event</a> for <var>registration</var>.
                  </li>
                  <li>
                    Abort the rest of these steps.
                  </li>
                </ol>
              </li>
              <li>
                <a>Assert</a>: <var>registration</var>'s <a>registration state</a> is <a>firing</a>.
              </li>
              <li>
                Remove <var>registration</var> from <var>serviceWorkerRegistration</var>'s <a>list of sync registration</a>.
              </li>
            </ol>
          </li>
          <li>
            <a>Upon rejection</a> of <var>waitUntilPromise</var>, or if the script has been aborted by the <a>termination</a> of the <a>service worker</a>, perform the following steps atomically:
            <ol>
              <li>
                If <var>registration</var>'s state is <a>reregisteredWhileFiring</a>:
                <ol>
                  <li>
                    Set <var>registration</var>'s state to <a>pending</a>.
                  </li>
                  <li>
                    If the user agent is currently <a>online</a>, <a>fire a sync event</a> for <var>registration</var>.
                  </li>
                  <li>
                    Abort the rest of these steps.
                  </li>
                </ol>
              </li>
              <li>
                If the {{SyncEvent/lastChance}} attribute of <var>dispatchedEvent</var> is false, set <var>registration</var>'s <a>registration state</a> to <a>waiting</a>, and perform the following steps <a>in parallel</a>:
                <ol>
                  <li>Wait a user agent defined length of time.</li>
                  <li>If <var>registration</var>'s <a>registration state</a> is not <a>waiting</a>, abort these substeps.</li>
                  <li>Set <var>registration</var>'s <a>registration state</a> to <a>pending</a>.</li>
                  <li>If the user agent is currently <a>online</a>, <a>fire a sync event</a> for <var>registration</var>.</li>
                </ol>
              </li>
              <li>
                Else remove <var>registration</var> from <var>serviceWorkerRegistration</var>'s <a>list of sync registrations</a>.
              </li>
            </ol>
          </li>
        </ol>
      </li>
    </ol>

    A user agent MAY impose a time limit on the lifetime extension and execution time of a {{SyncEvent}} which is stricter than the time limit imposed for {{ExtendableEvent}}s in general. In particular an event for which {{SyncEvent/lastChance}} is true MAY have a significantly shortened time limit.

    A user agent <dfn>will retry</dfn> a <a href="#sync-event">sync event</a> based on some user agent defined heuristics.
</section>
